Skip to main content

copp\copp\copp2\opt2/
copp2_socp.rs

1//! 2nd-order Convex-Objective Path Parameterization (COPP2) based on second-order cone programming (SOCP).
2//!
3//! # Method identity
4//! This module implements the **optimization backend** for COPP2 by transforming path-parameterization
5//! constraints/objectives into a Clarabel-compatible conic form and solving it with SOCP.
6//!
7//! # Discrete variables (local notation)
8//! On a path grid `s[0..=n]`:
9//! - `a[k]` denotes $\dot{s}_k^2$ (state variable, expected nonnegative in feasible solutions);
10//! - decision vector is organized as `x = [a[0..=n], x_others]`, where `x_others` are auxiliary variables introduced by objective terms (e.g. reciprocal/soc slack variables);
11//!
12//! # High-level pipeline
13//! 1. Validate interval and boundary consistency.
14//! 2. Estimate capacities and assemble standard TOPP2 constraints.
15//! 3. Add COPP2 objective-induced variables/cones (`Time`, `ThermalEnergy`, `TotalVariationTorque`, `Linear`).
16//! 4. Build sparse matrices `A`, `P`, vector `q`, and solve by Clarabel.
17//! 5. Apply status acceptance policy (`ClarabelOptions::is_allow`) and extract `a` when allowed.
18//!
19//! # API layering
20//! - `copp2_socp`: strict/normal API, returns only accepted `a`.
21//! - `copp2_socp_expert`: expert API, always returns full Clarabel solution for diagnosis.
22
23use crate::copp::clarabel_backend::{ConstraintsClarabel, ObjConsClarabel};
24use crate::copp::copp2::formulation::Copp2Problem;
25use crate::copp::copp2::opt2::clarabel_constraints::{
26    clarabel_standard_capacity_topp2, clarabel_standard_constraint_topp2,
27};
28use crate::copp::{ClarabelOptions, CoppObjective, clarabel_to_copp2_solution};
29use crate::diag::{
30    CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
31    format_duration_human,
32};
33use crate::robot::robot_core::{Robot, RobotBasic, RobotTorque};
34use clarabel::algebra::CscMatrix;
35use clarabel::solver::SupportedConeT::{NonnegativeConeT, SecondOrderConeT};
36use clarabel::solver::{DefaultSolution, DefaultSolver, IPSolver, SupportedConeT};
37use core::f64;
38use itertools::{Itertools, izip};
39use nalgebra::{DMatrix, DVectorView};
40
41#[cfg(test)]
42use crate::copp::copp2::stable::basic::a_to_b_topp2;
43
44/// Strict COPP2-SOCP API for production use.
45///
46/// # Purpose
47/// Use this entry when caller only needs a valid trajectory profile `a` and treats
48/// non-accepted solver statuses as hard failures.
49///
50/// # Contract
51/// - Internally calls [`copp2_socp_expert`].
52/// - Returns `Ok(a)` **iff** `options.is_allow(solution.status)` is `true`.
53/// - Returns `Err(CoppError::ClarabelSolverStatus(...))` when status is not accepted.
54///
55/// # Returns
56/// Returns accepted profile `a` for production usage.
57///
58/// # Errors
59/// Returns `CoppError` for model/solver failures and when solver status is not accepted.
60///
61/// # Notes
62/// For workflows requiring low-level diagnostics (`status`, iterate behavior, residual-related fields in
63/// Clarabel solution), prefer [`copp2_socp_expert`].
64pub fn copp2_socp<'a, M: RobotTorque>(
65    problem: &Copp2Problem<'a, M>,
66    options: &ClarabelOptions,
67) -> Result<Vec<f64>, CoppError> {
68    let (a_profile, solution) = copp2_socp_expert(problem, options)?;
69    a_profile.ok_or_else(|| CoppError::ClarabelSolverStatus("copp2_socp".into(), solution.status))
70}
71
72/// Expert COPP2-SOCP API with full Clarabel solution exposure.
73///
74/// # Purpose
75/// This API is intended for advanced users who need both:
76/// - extracted high-level profile `Option<Vec<f64>>`, and
77/// - raw solver result `DefaultSolution<f64>` for post-analysis.
78///
79/// # Return contract
80/// - `Ok((Some(a), solution))`: status accepted by `options.is_allow(solution.status)`.
81/// - `Ok((None, solution))`: solve finished but status not accepted by policy.
82/// - `Err(...)`: true runtime failures only (input validation / model build / solver construction).
83///
84/// # Returns
85/// Returns tuple `(Option<Vec<f64>>, DefaultSolution<f64>)` for diagnostic workflows.
86///
87/// # Errors
88/// Returns `CoppError` only for real failures (input, model build, or solver runtime).
89///
90/// # Contract
91/// - caller must handle `None` profile when status is not accepted;
92/// - acceptance policy is fully controlled by `options.is_allow`.
93///
94/// # Verbosity behavior
95/// Logging is layered by `options.verbosity()`:
96/// - `Silent`: no algorithm logs;
97/// - `Summary`: lifecycle milestones and elapsed time;
98/// - `Debug`: assembly-level counters and stage summaries;
99/// - `Trace`: fine-grained stage deltas and solver snapshot diagnostics.
100pub fn copp2_socp_expert<'a, M: RobotTorque>(
101    problem: &Copp2Problem<'a, M>,
102    options: &ClarabelOptions,
103) -> Result<(Option<Vec<f64>>, DefaultSolution<f64>), CoppError> {
104    match options.verbosity() {
105        Verbosity::Silent => copp2_socp_core(problem, (options, SilentVerboser)),
106        Verbosity::Summary => copp2_socp_core(problem, (options, SummaryVerboser::new())),
107        Verbosity::Debug => copp2_socp_core(problem, (options, DebugVerboser::new())),
108        Verbosity::Trace => copp2_socp_core(problem, (options, TraceVerboser::new())),
109    }
110}
111
112/// Core implementation for COPP2-SOCP expert flow.
113///
114/// # Internal contract
115/// `options_verboser` packs:
116/// - `options`: acceptance policy and Clarabel numerical settings;
117/// - `verboser`: concrete logger implementation chosen by external verbosity dispatch.
118///
119/// # Invariants
120/// - decision-variable layout always starts with contiguous `a[0..=n]`;
121/// - `q_object.len()` is treated as final `n_var` before solver build;
122/// - extracted `a` is produced only through `clarabel_to_copp2_solution` when status is accepted.
123fn copp2_socp_core<'a, M: RobotTorque>(
124    problem: &Copp2Problem<'a, M>,
125    options_verboser: (&ClarabelOptions, impl Verboser),
126) -> Result<(Option<Vec<f64>>, DefaultSolution<f64>), CoppError> {
127    let (options, mut verboser) = options_verboser;
128    let (idx_s_start, idx_s_final) = problem.idx_s_interval;
129    if verboser.is_enabled(Verbosity::Summary) {
130        verboser.record_start_time();
131        crate::verbosity_log!(
132            crate::diag::Verbosity::Summary,
133            "\ncopp2_socp started: {} <= idx_s <= {}, objectives = {}, s_len = {}.",
134            idx_s_start,
135            idx_s_final,
136            problem.objectives.len(),
137            problem.s_len()
138        );
139    }
140    if verboser.is_enabled(Verbosity::Trace) {
141        let settings = options.clarabel_settings();
142        crate::verbosity_log!(
143            crate::diag::Verbosity::Summary,
144            "copp2_socp: options snapshot -> allow(almost={}, max_iter={}, max_time={}, callback_term={}, insufficient_progress={}), tol_gap_rel={}, tol_feas={}, max_iter={}, verbose={}",
145            options.is_allow(clarabel::solver::SolverStatus::AlmostSolved),
146            options.is_allow(clarabel::solver::SolverStatus::MaxIterations),
147            options.is_allow(clarabel::solver::SolverStatus::MaxTime),
148            options.is_allow(clarabel::solver::SolverStatus::CallbackTerminated),
149            options.is_allow(clarabel::solver::SolverStatus::InsufficientProgress),
150            settings.tol_gap_rel,
151            settings.tol_feas,
152            settings.max_iter,
153            settings.verbose
154        );
155    }
156    // Check input validity
157    let n = idx_s_final - idx_s_start;
158    // Let x = [a[0], a[1], ..., a[n], x_others] \in R^{n+1+n_others}.
159    // Step 1. Deal with constraints
160    // Step 1.1 Compute the number of constraints
161    let (cap_val_std, cap_b_std, cap_cone_std) =
162        clarabel_standard_capacity_topp2(&problem.robot.constraints, problem.idx_s_interval);
163    let (cap_val_obj, cap_b_obj, cap_cone_obj, n_vars) =
164        clarabel_objective_capacity_copp2(n, problem.objectives, problem.robot);
165    if verboser.is_enabled(Verbosity::Debug) {
166        crate::verbosity_log!(
167            crate::diag::Verbosity::Summary,
168            "copp2_socp: capacity estimate std(val={cap_val_std}, b={cap_b_std}, cone={cap_cone_std}), obj(val={cap_val_obj}, b={cap_b_obj}, cone={cap_cone_obj}), n_vars={n_vars}."
169        );
170    }
171    // s=b-A*x \in cone, where A[row[i],col[i]]=val[i], A \in R^{m*(n+1)}, b \in R^m, s \in R^m
172    // -s=-b+A*x
173    let mut row = Vec::<usize>::with_capacity(cap_val_std + cap_val_obj);
174    let mut col = Vec::<usize>::with_capacity(cap_val_std + cap_val_obj);
175    let mut val = Vec::<f64>::with_capacity(cap_val_std + cap_val_obj);
176    let mut b = Vec::<f64>::with_capacity(cap_b_std + cap_b_obj);
177    let mut cones = Vec::<SupportedConeT<f64>>::with_capacity(cap_cone_std + cap_cone_obj);
178    if verboser.is_enabled(Verbosity::Trace) {
179        crate::verbosity_log!(
180            crate::diag::Verbosity::Summary,
181            "copp2_socp: allocated capacities row/col/val/b/cones <= {}/{}/{}/{}/{}",
182            cap_val_std + cap_val_obj,
183            cap_val_std + cap_val_obj,
184            cap_val_std + cap_val_obj,
185            cap_b_std + cap_b_obj,
186            cap_cone_std + cap_cone_obj
187        );
188    }
189    // Step 1.2 set constraints
190    let row_before_std = row.len();
191    let col_before_std = col.len();
192    let val_before_std = val.len();
193    let b_before_std = b.len();
194    let cones_before_std = cones.len();
195    clarabel_standard_constraint_topp2(
196        &problem.as_topp2_problem(),
197        (&mut row, &mut col, &mut val, &mut b, &mut cones),
198        &verboser,
199    );
200    if verboser.is_enabled(Verbosity::Trace) {
201        crate::verbosity_log!(
202            crate::diag::Verbosity::Summary,
203            "copp2_socp: standard-constraints delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}",
204            row.len() - row_before_std,
205            col.len() - col_before_std,
206            val.len() - val_before_std,
207            b.len() - b_before_std,
208            cones.len() - cones_before_std
209        );
210    }
211    // Step 2. set objective
212    // Step 2.1. determine whether eta=1/sqrt(a) is needed.
213    let row_before_sqrt = row.len();
214    let col_before_sqrt = col.len();
215    let val_before_sqrt = val.len();
216    let b_before_sqrt = b.len();
217    let cones_before_sqrt = cones.len();
218    let n_var_old = clarabel_sqrt_a_copp2(
219        n,
220        problem.objectives,
221        (&mut row, &mut col, &mut val, &mut b, &mut cones),
222    );
223    if verboser.is_enabled(Verbosity::Trace) {
224        crate::verbosity_log!(
225            crate::diag::Verbosity::Summary,
226            "copp2_socp: sqrt-a stage delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}, n_var_old={}",
227            row.len() - row_before_sqrt,
228            col.len() - col_before_sqrt,
229            val.len() - val_before_sqrt,
230            b.len() - b_before_sqrt,
231            cones.len() - cones_before_sqrt,
232            n_var_old
233        );
234    }
235    let mut q_object = Vec::<f64>::with_capacity(n_vars);
236    q_object.resize(n_var_old, 0.0);
237    // Step 2.2. add constraints and objective for each term in the objective.
238    let row_before_obj = row.len();
239    let col_before_obj = col.len();
240    let val_before_obj = val.len();
241    let b_before_obj = b.len();
242    let cones_before_obj = cones.len();
243    let q_before_obj = q_object.len();
244    clarable_objective_copp2(
245        problem,
246        (
247            &mut row,
248            &mut col,
249            &mut val,
250            &mut b,
251            &mut cones,
252            &mut q_object,
253        ),
254    )?;
255    if verboser.is_enabled(Verbosity::Trace) {
256        let (q_min, q_max) = q_object
257            .iter()
258            .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), &v| {
259                (mn.min(v), mx.max(v))
260            });
261        crate::verbosity_log!(
262            crate::diag::Verbosity::Summary,
263            "copp2_socp: objective stage delta row/col/val/b/cones/q = +{}/+{}/+{}/+{}/+{}/+{}, q_range=[{}, {}]",
264            row.len() - row_before_obj,
265            col.len() - col_before_obj,
266            val.len() - val_before_obj,
267            b.len() - b_before_obj,
268            cones.len() - cones_before_obj,
269            q_object.len() - q_before_obj,
270            q_min,
271            q_max
272        );
273    }
274    if verboser.is_enabled(Verbosity::Debug) {
275        crate::verbosity_log!(
276            crate::diag::Verbosity::Summary,
277            "copp2_socp: after objective assembly row={}, col={}, val={}, b={}, cones={}, q={}",
278            row.len(),
279            col.len(),
280            val.len(),
281            b.len(),
282            cones.len(),
283            q_object.len()
284        );
285    }
286    if verboser.is_enabled(Verbosity::Summary) {
287        crate::verbosity_log!(
288            crate::diag::Verbosity::Summary,
289            "copp2_socp: ready to solve with row/col/val/b/cones = {}/{}/{}/{}/{} and n_var = {}.",
290            row.len(),
291            col.len(),
292            val.len(),
293            b.len(),
294            cones.len(),
295            q_object.len()
296        );
297    }
298    // Step 2.3 build the constraints
299    let n_var = q_object.len();
300    let a_csc = CscMatrix::new_from_triplets(b.len(), n_var, row, col, val);
301    let p_object = CscMatrix::<f64>::zeros((n_var, n_var));
302    if verboser.is_enabled(Verbosity::Trace) {
303        crate::verbosity_log!(
304            crate::diag::Verbosity::Summary,
305            "copp2_socp: matrix built with m={}, n={}, A.nnz={}, P.nnz={}",
306            b.len(),
307            n_var,
308            a_csc.nnz(),
309            p_object.nnz()
310        );
311    }
312    // Step 3. solve the SOCP problem
313    let settings = options.clarabel_settings().clone();
314    let mut solver = DefaultSolver::<f64>::new(&p_object, &q_object, &a_csc, &b, &cones, settings)
315        .map_err(|e| CoppError::ClarabelSolverError("copp2_socp".into(), e))?;
316    solver.solve();
317    let solution = solver.solution;
318    if verboser.is_enabled(Verbosity::Summary) {
319        crate::verbosity_log!(
320            crate::diag::Verbosity::Summary,
321            "copp2_socp: solve done, status = {:?}, elapsed = {}.",
322            solution.status,
323            format_duration_human(verboser.elapsed())
324        );
325    }
326    if verboser.is_enabled(Verbosity::Trace) {
327        let show = solution.x.len().min(3);
328        crate::verbosity_log!(
329            crate::diag::Verbosity::Summary,
330            "copp2_socp: solution x_len={}, head={:?}",
331            solution.x.len(),
332            &solution.x[0..show]
333        );
334    }
335    let a_profile = if options.is_allow(solution.status) {
336        Some(clarabel_to_copp2_solution(problem.s_len(), &solution))
337    } else {
338        None
339    };
340    if verboser.is_enabled(Verbosity::Trace) {
341        crate::verbosity_log!(
342            crate::diag::Verbosity::Summary,
343            "copp2_socp: allow(status)={}, extracted_profile={}",
344            options.is_allow(solution.status),
345            if a_profile.is_some() {
346                "Some(a)"
347            } else {
348                "None"
349            }
350        );
351    }
352    Ok((a_profile, solution))
353}
354
355/// Determine the number of clarabel's capacity for the objective in COPP2.
356fn clarabel_objective_capacity_copp2<M: RobotBasic>(
357    n: usize,
358    objective: &[CoppObjective],
359    robot: &Robot<M>,
360) -> (usize, usize, usize, usize) {
361    let flag_need_eta = objective.iter().any(|obj| {
362        matches!(
363            obj,
364            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _)
365        )
366    });
367    // Step 1. sqrt(a[k]) >= eta[k] >= 0
368    // num_val <= 4*(n+1), num_b <= 4*(n+1), num_cones <= n+2
369    let (mut capacity_val, mut capacity_b, mut capacity_cones, mut n_vars) = if flag_need_eta {
370        (4 * (n + 1), 4 * (n + 1), n + 2, 2 * (n + 1))
371    } else {
372        (0, 0, 0, n + 1)
373    };
374    // Step 2. objective function
375    let dim = robot.dim();
376    for obj in objective {
377        match obj {
378            CoppObjective::Time(_) => {
379                // num_val <= 6*n, num_b <= 3*n, num_cones <= n, n_var <= n
380                capacity_val += 6 * n;
381                capacity_b += 3 * n;
382                capacity_cones += n;
383                n_vars += n;
384            }
385            CoppObjective::ThermalEnergy(_, _) => {
386                // num_val <= (6+2*dim)*n, num_b <= (dim+2)*n, num_cones <= n, n_var <= n
387                capacity_val += (6 + 2 * dim) * n;
388                capacity_b += (dim + 2) * n;
389                capacity_cones += n;
390                n_vars += n;
391            }
392            CoppObjective::TotalVariationTorque(_, _) => {
393                // num_val <= 8*dim*n, num_b <= 2*dim*n, num_cones <= 1, n_var <= dim*n
394                capacity_val += 8 * dim * n;
395                capacity_b += 2 * dim * n;
396                capacity_cones += 1;
397                n_vars += dim * n;
398            }
399            _ => {}
400        }
401    }
402    (capacity_val, capacity_b, capacity_cones, n_vars)
403}
404
405/// Add the constraints for sqrt(a) >= eta in COPP2 optimization.  
406/// x = [a[0], a[1], ..., a[n], eta[0], eta[1], ..., eta[n], ...] \in R^{2*(n+1)+...}.
407/// sqrt(a[k]) >= eta[k] >= 0  
408/// num_val <= 4*(n+1), num_b <= 4*(n+1), num_cones <= n+2  
409/// Return the len of the new x: n+1 or 2*(n+1)
410fn clarabel_sqrt_a_copp2(
411    n: usize,
412    objective: &[CoppObjective],
413    constraints: ConstraintsClarabel,
414) -> usize {
415    let (row, col, val, b, cones) = constraints;
416    for obj in objective {
417        match obj {
418            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _) => {
419                // eta >= 0
420                // A*x-b = -s = -1*eta[k] <= 0
421                row.extend(b.len()..b.len() + n + 1);
422                col.extend((n + 1)..(2 * (n + 1)));
423                val.resize(val.len() + n + 1, -1.0);
424                b.resize(b.len() + n + 1, 0.0);
425                cones.push(NonnegativeConeT(n + 1));
426                // sqrt(a) >= eta
427                // eta^2 <= a
428                // eta^2 + (a - 0.25)^2 <= (a + 0.25)^2
429                // -A*x+b = s = [a+0.25, a-0.25, eta] \in SOC
430                row.extend(b.len()..b.len() + 3 * (n + 1));
431                val.resize(val.len() + 3 * (n + 1), -1.0);
432                cones.resize(cones.len() + n + 1, SecondOrderConeT(3));
433                for k in 0..=n {
434                    // row.extend(b.len()..b.len() + 3);
435                    col.extend([k, k, k + n + 1]);
436                    // val.resize(val.len() + 3, -1.0);
437                    b.extend([0.25, -0.25, 0.0]);
438                    // cones.push(SecondOrderConeT(3));
439                }
440                return 2 * (n + 1);
441            }
442            _ => {}
443        }
444    }
445    n + 1
446}
447
448/// Add the constraints and objective for Time in COPP2 optimization.  
449/// num_val <= 6*n, num_b <= 3*n, num_cones <= n, n_var <= n
450fn clarabel_objective_time_copp2(
451    s: &[f64],
452    weight: f64,
453    objective_constraints: ObjConsClarabel,
454) -> bool {
455    if weight < 0.0 {
456        return false;
457    }
458    let (row, col, val, b, cones, q_object) = objective_constraints;
459    // objective: minimize 2 * weight * \sum (s[k+1]-s[k]) / (eta[k] + eta[k+1])
460    // Let: 1 / (eta[k] + eta[k+1]) <= 4 * t[k]
461    // objective: minimize 8 * weight * \sum (s[k+1]-s[k]) * t[k]
462    let weight = 8.0 * weight;
463    let n_var_old = q_object.len();
464    // objective: minimize weight * \sum (s[k+1]-s[k]) * t[k]
465    q_object.extend(s.windows(2).map(|s_pair| weight * (s_pair[1] - s_pair[0])));
466    // t[k] * (eta[k] + eta[k+1]) >= 1
467    // (eta[k] + eta[k+1] + t[k])^2 >= (eta[k] + eta[k+1] - t[k])^2 + 1
468    // -A*x+b = s = [eta[k] + eta[k+1] + t[k], eta[k] + eta[k+1] - t[k], 1] \in SOC
469    let len = s.len();
470    for k in 0..(len - 1) {
471        // eta[k] + eta[k+1] + t[k]
472        row.resize(row.len() + 3, b.len());
473        col.extend([len + k, len + k + 1, n_var_old + k]);
474        val.extend([-1.0, -1.0, -1.0]);
475        b.push(0.0);
476        // eta[k] + eta[k+1] - t[k]
477        row.resize(row.len() + 3, b.len());
478        col.extend([len + k, len + k + 1, n_var_old + k]);
479        val.extend([-1.0, -1.0, 1.0]);
480        b.push(0.0);
481        // 1
482        b.push(1.0);
483        // cones.push(SecondOrderConeT(3));
484    }
485    cones.resize(cones.len() + len - 1, SecondOrderConeT(3));
486    true
487}
488
489/// Add the constraints and objective for ThermalEnergy in COPP2 optimization.  
490/// num_val <= (6+2*dim)*n, num_b <= (dim+2)*n, num_cones <= n, n_var <= n
491fn clarabel_objective_thermal_energy_copp2(
492    s: &[f64],
493    weight: f64,
494    normalize: &[f64],
495    coeffs_torque: &(DMatrix<f64>, DMatrix<f64>, DMatrix<f64>),
496    objective_constraints: ObjConsClarabel,
497) -> bool {
498    if weight < 0.0 {
499        return false;
500    }
501    let (row, col, val, b, cones, q_object) = objective_constraints;
502    // minimize: 2 * weight * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1])) * (tau[i][k] * normalize[i]) ^ 2
503    // minimize: 2 * weight * \sum (s[k+1]-s[k]) / (eta[k] + eta[k+1]) * (tau[i][k] * normalize[i]) ^ 2
504    // Let: \sum_i (tau[i][k] * normalize[i]) ^ 2 / (eta[k] + eta[k+1]) <= 4 * t[k]
505    let len = s.len();
506    let mut coeff_a_curr = coeffs_torque.0.clone();
507    let mut coeff_a_next = coeffs_torque.1.clone();
508    let mut coeff_g = coeffs_torque.2.clone();
509    // objective: minimize 8 * weight * \sum (s[k+1]-s[k]) * t[k]
510    let weight = 8.0 * weight;
511    let n_var_old = q_object.len();
512    // objective: minimize weight * \sum (s[k+1]-s[k]) * t[k]
513    q_object.extend(s.windows(2).map(|s_pair| weight * (s_pair[1] - s_pair[0])));
514    // \sum_i (tau[i][k] * normalize[i]) ^ 2 <= 4 * t[k] * (eta[k] + eta[k+1])
515    let dim = coeff_a_curr.nrows();
516    // tau[i][k] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
517    if normalize.len() != dim {
518        return false;
519    }
520    let normalize = DVectorView::from_slice(normalize, dim);
521    for mut col in coeff_a_curr.column_iter_mut() {
522        col.component_mul_assign(&normalize);
523    }
524    for mut col in coeff_a_next.column_iter_mut() {
525        col.component_mul_assign(&normalize);
526    }
527    for mut col in coeff_g.column_iter_mut() {
528        col.component_mul_assign(&normalize);
529    }
530    // Now: tau[i][k] * normalize[i] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
531
532    // (eta[k] + eta[k+1] - t[k])^2 + \sum_i (tau[i][k] * normalize[i]) ^ 2 <= (eta[k] + eta[k+1] + t[k])^2
533    // -A*x+b = s = [eta[k] + eta[k+1] + t[k], eta[k] + eta[k+1] - t[k], tau[0][k] * normalize[0], tau[1][k] * normalize[1], ...] \in SOC
534    for (k, (col_a_curr, col_a_next, col_g)) in izip!(
535        coeff_a_curr.column_iter(),
536        coeff_a_next.column_iter(),
537        coeff_g.column_iter()
538    )
539    .enumerate()
540    {
541        // eta[k] + eta[k+1] + t[k]
542        row.resize(row.len() + 3, b.len());
543        col.extend([len + k, len + k + 1, n_var_old + k]);
544        val.extend([-1.0, -1.0, -1.0]);
545        b.push(0.0);
546        // eta[k] + eta[k+1] - t[k]
547        row.resize(row.len() + 3, b.len());
548        col.extend([len + k, len + k + 1, n_var_old + k]);
549        val.extend([-1.0, -1.0, 1.0]);
550        b.push(0.0);
551        // tau[i][k] * normalize[i] = col_a_curr[i] * x[k] + col_a_next[i] * x[k+1] + col_g[i]
552        for (&v_a_curr, &v_a_next, &v_g) in
553            izip!(col_a_curr.iter(), col_a_next.iter(), col_g.iter())
554        {
555            row.resize(row.len() + 2, b.len());
556            col.extend([k, k + 1]);
557            val.extend([v_a_curr, v_a_next]);
558            b.push(v_g);
559        }
560    }
561    cones.resize(cones.len() + len - 1, SecondOrderConeT(dim + 2));
562    true
563}
564
565/// Add the constraints and objective for TotalVariationTorque in COPP2 optimization.  
566/// num_val <= 8*dim*n, num_b <= 2*dim*n, num_cones <= 1, n_var <= dim*n
567fn clarabel_objective_tv_torque_copp2(
568    weight: f64,
569    normalize: &[f64],
570    coeffs_torque: &(DMatrix<f64>, DMatrix<f64>, DMatrix<f64>),
571    objective_constraints: ObjConsClarabel,
572) -> bool {
573    if weight < 0.0 {
574        return false;
575    }
576    let (row, col, val, b, cones, q_object) = objective_constraints;
577    // minimize: weight * \sum |tau[i][k+1]-tau[i][k]| * normalize[i]
578    // Let: |tau[i][k+1]-tau[i][k]| * normalize[i] <= t[i][k]
579    let mut coeff_a_curr = coeffs_torque.0.clone();
580    let mut coeff_a_next = coeffs_torque.1.clone();
581    let mut coeff_g = coeffs_torque.2.clone();
582    let dim = coeff_a_curr.nrows();
583    if normalize.len() != dim {
584        return false;
585    }
586    // tau[i][k] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
587    let normalize = DVectorView::from_slice(normalize, dim);
588    for mut col in coeff_a_curr.column_iter_mut() {
589        col.component_mul_assign(&normalize);
590    }
591    for mut col in coeff_a_next.column_iter_mut() {
592        col.component_mul_assign(&normalize);
593    }
594    for mut col in coeff_g.column_iter_mut() {
595        col.component_mul_assign(&normalize);
596    }
597    // Now: tau[i][k] * normalize[i] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
598    // (tau[i][k+1]-tau[i][k]) * normalize[i] = (coeff_a_curr[i][k+1] * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + coeff_g[i][k+1]) - (coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k])
599    // = -coeff_a_curr[i][k] * a[k] + (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + (coeff_g[i][k+1] - coeff_g[i][k])
600
601    let n_b_old = b.len();
602    // A*x-b = -s = -coeff_a_curr[i][k] * a[k] + (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + (coeff_g[i][k+1] - coeff_g[i][k]) - t[i][k] <= 0
603    // A*x-b = -s = coeff_a_curr[i][k] * a[k] - (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] - coeff_a_next[i][k+1] * a[k+2] - (coeff_g[i][k+1] - coeff_g[i][k]) - t[i][k] <= 0
604    let mut buffer0 = vec![0.0; dim];
605    let mut buffer1 = vec![0.0; dim];
606    for (k, ((col_a_curr, col_b_curr, col_g_curr), (col_a_next, col_b_next, col_g_next))) in izip!(
607        coeff_a_curr.column_iter(),
608        coeff_a_next.column_iter(),
609        coeff_g.column_iter()
610    )
611    .tuple_windows()
612    .enumerate()
613    {
614        // dtau[i] * normalize[i] = -col_a_curr[i] * a[k] + (col_a_next[i] - col_b_curr[i]) * a[k+1] + col_b_next[i] * a[k+2] + (col_g_next[i] - col_g_curr[i])
615        buffer0.clear();
616        buffer1.clear();
617        buffer0.extend(
618            col_a_next
619                .iter()
620                .zip(col_b_curr.iter())
621                .map(|(&v_a_next, &v_b_curr)| v_b_curr - v_a_next),
622        );
623        buffer1.extend(
624            col_g_curr
625                .iter()
626                .zip(col_g_next.iter())
627                .map(|(&v_g_curr, &v_g_next)| v_g_curr - v_g_next),
628        );
629        // dtau[i] * normalize[i] = -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i]
630
631        let n_var_old = q_object.len();
632        for (i, (&v0, &v1, &v_a_curr, &v_b_next)) in izip!(
633            buffer0.iter(),
634            buffer1.iter(),
635            col_a_curr.iter(),
636            col_b_next.iter()
637        )
638        .enumerate()
639        {
640            // -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i] <= t[i][k]
641            // A*x-b = -s = -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i] - t[i][k] <= 0
642            row.resize(row.len() + 4, b.len());
643            col.extend([k, k + 1, k + 2, n_var_old + i]);
644            val.extend([-v_a_curr, v0, v_b_next, -1.0]);
645            b.push(v1);
646
647            // -(-col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2]) <= t[i][k]
648            // A*x-b = -s = -(-col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2]) - t[i][k] <= 0
649            row.resize(row.len() + 4, b.len());
650            col.extend([k, k + 1, k + 2, n_var_old + i]);
651            val.extend([v_a_curr, -v0, -v_b_next, -1.0]);
652            b.push(-v1);
653        }
654
655        // objective: minimize weight * \sum t[i][k]
656        q_object.resize(q_object.len() + dim, weight);
657    }
658    cones.push(NonnegativeConeT(b.len() - n_b_old));
659    true
660}
661
662/// Add the constraints and objective for Linear in COPP2 optimization.
663fn clarabel_objective_linear_copp2(
664    s: &[f64],
665    weight: f64,
666    alpha: &[f64],
667    beta: &[f64],
668    q_object: &mut [f64],
669) -> bool {
670    if alpha.len() != s.len() || beta.len() != s.len() - 1 {
671        return false;
672    }
673    // objective: minimize weight * \sum (alpha[k]*a[k] + beta[k]*b[k])
674    // weight * \sum alpha[k]*a[k] + 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
675    for (va, q) in alpha.iter().zip(q_object.iter_mut()) {
676        // weight * \sum alpha[k]*a[k]
677        *q += weight * va;
678    }
679    for (s_pair, vb, q_curr) in izip!(s.windows(2), beta.iter(), q_object.iter_mut()) {
680        // weight * \sum 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
681        *q_curr -= 0.5 * weight * vb / (s_pair[1] - s_pair[0]);
682    }
683    for (s_pair, vb, q_next) in izip!(s.windows(2), beta.iter(), q_object.iter_mut().skip(1)) {
684        // weight * \sum 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
685        *q_next += 0.5 * weight * vb / (s_pair[1] - s_pair[0]);
686    }
687    true
688}
689
690fn clarable_objective_copp2<M: RobotTorque>(
691    problem: &Copp2Problem<M>,
692    objective_constraints: ObjConsClarabel,
693) -> Result<(), CoppError> {
694    let (row, col, val, b, cones, q_object) = objective_constraints;
695    let s = problem
696        .robot
697        .constraints
698        .s_vec(problem.idx_s_interval.0, problem.idx_s_interval.1 + 1)?;
699    let coeffs_torque = if problem.objectives.iter().any(|obj| {
700        matches!(
701            obj,
702            CoppObjective::ThermalEnergy(_, _) | CoppObjective::TotalVariationTorque(_, _)
703        )
704    }) {
705        // shape: (dim, n) since there are n+1 a and n b.
706        problem.robot.torque2_coeff_a(
707            problem.idx_s_interval.0,
708            problem.idx_s_interval.1 - problem.idx_s_interval.0,
709        )
710    } else {
711        (
712            DMatrix::<f64>::zeros(0, 0),
713            DMatrix::<f64>::zeros(0, 0),
714            DMatrix::<f64>::zeros(0, 0),
715        )
716    };
717    for obj in problem.objectives {
718        match obj {
719            CoppObjective::Time(weight) => {
720                if !clarabel_objective_time_copp2(&s, *weight, (row, col, val, b, cones, q_object))
721                {
722                    return Err(CoppError::InvalidInput(
723                        "copp2_socp".into(),
724                        "Invalid Time objective.".into(),
725                    ));
726                }
727            }
728            CoppObjective::ThermalEnergy(weight, normalize) => {
729                if !clarabel_objective_thermal_energy_copp2(
730                    &s,
731                    *weight,
732                    normalize,
733                    &coeffs_torque,
734                    (row, col, val, b, cones, q_object),
735                ) {
736                    return Err(CoppError::InvalidInput(
737                        "copp2_socp".into(),
738                        "Invalid ThermalEnergy objective.".into(),
739                    ));
740                }
741            }
742            CoppObjective::TotalVariationTorque(weight, normalize) => {
743                if !clarabel_objective_tv_torque_copp2(
744                    *weight,
745                    normalize,
746                    &coeffs_torque,
747                    (row, col, val, b, cones, q_object),
748                ) {
749                    return Err(CoppError::InvalidInput(
750                        "copp2_socp".into(),
751                        "Invalid TotalVariationTorque objective.".into(),
752                    ));
753                }
754            }
755            CoppObjective::Linear(weight, alpha, beta) => {
756                if !clarabel_objective_linear_copp2(&s, *weight, alpha, beta, q_object) {
757                    return Err(CoppError::InvalidInput(
758                        "copp2_socp".into(),
759                        "Invalid Linear objective.".into(),
760                    ));
761                }
762            }
763        }
764    }
765    Ok(())
766}
767
768/// Compute the objective value for COPP2 optimization.
769#[cfg(test)]
770pub(crate) fn objective_value_copp2_opt<M: RobotTorque>(
771    robot: &Robot<M>,
772    start_idx_s: usize,
773    objective: &[CoppObjective],
774    a_profile: &[f64],
775) -> (f64, Vec<f64>) {
776    let Ok(s) = robot
777        .constraints
778        .s_vec(start_idx_s, start_idx_s + a_profile.len())
779    else {
780        return (f64::INFINITY, vec![0.0; objective.len()]);
781    };
782    if a_profile.len() != s.len() {
783        return (f64::INFINITY, vec![0.0; objective.len()]);
784    }
785    let b_profile = a_to_b_topp2(&s, a_profile);
786    let torque = if objective.iter().any(|obj| {
787        matches!(
788            obj,
789            CoppObjective::ThermalEnergy(_, _) | CoppObjective::TotalVariationTorque(_, _)
790        )
791    }) {
792        let torque_result =
793            robot.get_torque_with_ab(&a_profile[0..a_profile.len() - 1], &b_profile, start_idx_s);
794        match torque_result {
795            Ok(torque) => torque,
796            _ => return (f64::INFINITY, vec![0.0; objective.len()]),
797        }
798    } else {
799        DMatrix::<f64>::zeros(0, 0)
800    };
801    let a_sqrt = if objective.iter().any(|obj| {
802        matches!(
803            obj,
804            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _)
805        )
806    }) {
807        a_profile.iter().map(|a| a.sqrt()).collect()
808    } else {
809        Vec::new()
810    };
811    let mut obj_val = Vec::with_capacity(objective.len());
812    let mut obj_val_total = 0.0;
813    for obj in objective {
814        match obj {
815            CoppObjective::Time(weight) => {
816                let obj_here = objective_value_time_copp2(&s, &a_sqrt);
817                obj_val.push(obj_here);
818                obj_val_total += weight * obj_here;
819            }
820            CoppObjective::ThermalEnergy(weight, normalize) => {
821                let obj_here =
822                    objective_value_thermal_energy_copp2(&s, &a_sqrt, &torque, normalize);
823                obj_val.push(obj_here);
824                obj_val_total += weight * obj_here;
825            }
826            CoppObjective::TotalVariationTorque(weight, normalize) => {
827                let obj_here = objective_value_tv_torque_copp2(&torque, normalize);
828                obj_val.push(obj_here);
829                obj_val_total += weight * obj_here;
830            }
831            CoppObjective::Linear(weight, alpha, beta) => {
832                let obj_here = objective_value_linear_copp2(&s, a_profile, alpha, beta);
833                obj_val.push(obj_here);
834                obj_val_total += weight * obj_here;
835            }
836        }
837    }
838    (obj_val_total, obj_val)
839}
840
841/// Compute the time value in COPP2 optimization.  
842/// Input: s, a_sqrt = sqrt(a)  
843#[cfg(test)]
844#[inline(always)]
845fn objective_value_time_copp2(s: &[f64], a_sqrt: &[f64]) -> f64 {
846    // objective: minimize 2 * weight * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1]))
847    let mut objective = 0.0;
848    for (s_pair, a_sqrt_pair) in s.windows(2).zip(a_sqrt.windows(2)) {
849        objective += (s_pair[1] - s_pair[0]) / (a_sqrt_pair[0] + a_sqrt_pair[1]);
850    }
851    2.0 * objective
852}
853
854/// Compute the thermal energy value in COPP2 optimization.
855#[cfg(test)]
856#[inline(always)]
857fn objective_value_thermal_energy_copp2(
858    s: &[f64],
859    a_sqrt: &[f64],
860    torque: &DMatrix<f64>,
861    normalize: &[f64],
862) -> f64 {
863    // minimize: 2 * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1])) * (tau[i][k] * normalize[i]) ^ 2
864    let mut objective = 0.0;
865    for (s_pair, a_sqrt_pair, torque_col) in
866        izip!(s.windows(2), a_sqrt.windows(2), torque.column_iter())
867    {
868        let mut sum = 0.0;
869        for (torque, normal) in torque_col.iter().zip(normalize.iter()) {
870            sum += (torque * normal).powi(2);
871        }
872        objective += (s_pair[1] - s_pair[0]) / (a_sqrt_pair[0] + a_sqrt_pair[1]) * sum;
873    }
874    2.0 * objective
875}
876
877/// Compute the total variation of torque value in COPP2 optimization.
878#[cfg(test)]
879#[inline(always)]
880fn objective_value_tv_torque_copp2(torque: &DMatrix<f64>, normalize: &[f64]) -> f64 {
881    // minimize: weight * \sum |tau[i][k+1]-tau[i][k]| * normalize[i]
882    let mut objective = 0.0;
883    for (torque_col_curr, torque_col_next) in torque.column_iter().tuple_windows() {
884        for (torque_prev, torque_next, normal) in izip!(
885            torque_col_curr.iter(),
886            torque_col_next.iter(),
887            normalize.iter()
888        ) {
889            objective += (torque_next - torque_prev).abs() * normal;
890        }
891    }
892    objective
893}
894
895/// Compute the objective value for Linear in COPP2 optimization.
896#[cfg(test)]
897#[inline(always)]
898fn objective_value_linear_copp2(s: &[f64], a_profile: &[f64], alpha: &[f64], beta: &[f64]) -> f64 {
899    // objective: minimize \sum (alpha[k]*a[k] + beta[k]*b[k])
900    // \sum alpha[k]*a[k] + 0.5*beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
901    let mut objective = 0.0;
902    for (a_curr, alpha_curr) in a_profile.iter().zip(alpha.iter()) {
903        // alpha[k]*a[k]
904        objective += a_curr * alpha_curr;
905    }
906    for (a_pair, s_pair, beta_curr) in izip!(a_profile.windows(2), s.windows(2), beta.iter()) {
907        // 0.5*beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
908        objective += 0.5 * beta_curr * (a_pair[1] - a_pair[0]) / (s_pair[1] - s_pair[0]);
909    }
910    objective
911}
912
913#[cfg(test)]
914mod tests {
915    use super::*;
916    use crate::copp::copp2::stable::basic::{
917        Copp2ProblemBuilder, Topp2ProblemBuilder, s_to_t_topp2,
918    };
919    use crate::copp::copp2::stable::reach_set2::{ReachSet2Options, ReachSet2OptionsBuilder};
920    use crate::copp::copp2::stable::topp2_ra::topp2_ra;
921    use crate::copp::{ClarabelOptions, ClarabelOptionsBuilder};
922    use crate::path::{add_symmetric_axial_limits_for_test, lissajous_path_for_test};
923    use crate::robot::demo::Plannar2LinkEnd;
924    use crate::robot::robot_core::Robot;
925    use core::panic;
926    use std::time::Instant;
927    use std::vec;
928
929    #[test]
930    fn test_copp2_socp_only_time() -> Result<(), CoppError> {
931        run_test_copp2_socp_only_time_repeated(1, false)
932    }
933
934    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
935    /// Average 100 experiments: tc_ra = 0.2005 ms, tc_lp = 29.7361 ms, tc_qp = 166.5122 ms, tf_ra = 4.766745, tf_lp = 4.766745, tf_qp = 4.766735
936    #[test]
937    #[ignore = "slow"]
938    fn test_copp2_socp_only_time_robust() -> Result<(), CoppError> {
939        run_test_copp2_socp_only_time_repeated(100, true)?;
940        Ok(())
941    }
942
943    #[test]
944    fn test_copp2_socp() -> Result<(), CoppError> {
945        run_test_copp2_socp_repeated(1, false)
946    }
947
948    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
949    /// Average 100 experiments:
950    //  Case 0: tc=161.691ms, obj=[4.824561313613876, 2576.8873693126284, 71.69242263342399, -1.048938713665848e-14]
951    //  Case 1: tc=200.959ms, obj=[4.830121022357044, 2576.928260526994, 66.14588916597324, -7.651101974204267e-15]
952    //  Case 2: tc=218.223ms, obj=[4.830230167437296, 2576.9529624501106, 66.12305109242398, -1.09470765785602e-14]
953    //  Case 3: tc=93.588ms, obj=[361.45110144118144, 194870.1172166501, 48.05756488144189, 9.743247875171334e-16]
954    //  Case 4: tc=164.505ms, obj=[4.824561266838617, 2576.887346690997, 71.69241993761281, 2.7478852526741092e-14]
955    #[test]
956    #[ignore = "slow"]
957    fn test_copp2_socp_robust() -> Result<(), CoppError> {
958        run_test_copp2_socp_repeated(100, true)?;
959        Ok(())
960    }
961
962    fn run_test_copp2_socp_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
963        let options_socp = ClarabelOptionsBuilder::new()
964            .allow_almost_solved(true)
965            .build()?;
966
967        let mut tc_sum_case0 = 0.0;
968        let mut tc_sum_case1 = 0.0;
969        let mut tc_sum_case2 = 0.0;
970        let mut tc_sum_case3 = 0.0;
971        let mut tc_sum_case4 = 0.0;
972        let mut obj_sum_case0 = vec![0.0; 4];
973        let mut obj_sum_case1 = vec![0.0; 4];
974        let mut obj_sum_case2 = vec![0.0; 4];
975        let mut obj_sum_case3 = vec![0.0; 4];
976        let mut obj_sum_case4 = vec![0.0; 4];
977
978        for i_exp in 0..n_exp {
979            let n: usize = 1000;
980            let mut robot = Robot::with_capacity(Plannar2LinkEnd::new(1.0, 1.0, 1.0, 1.0), n);
981            let dim = robot.dim();
982
983            let mut rng = rand::rng();
984            let (s, derivs, omega, phi) =
985                lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
986            robot.with_s(&s.as_view())?;
987            robot.with_q(
988                &derivs.q.as_view(),
989                &derivs.dq.as_ref().unwrap().as_view(),
990                &derivs.ddq.as_ref().unwrap().as_view(),
991                derivs.dddq.as_ref().map(|m| m.as_view()).as_ref(),
992                0,
993            )?;
994            add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
995
996            // Test different objectives in COPP2 optimization
997            let objectives_test = [
998                CoppObjective::Time(1.0),
999                CoppObjective::ThermalEnergy(1.0, &vec![1.0; dim]),
1000                CoppObjective::TotalVariationTorque(1.0, &vec![1.0; dim]),
1001                CoppObjective::Linear(1.0, &vec![0.0; n], &vec![1.0; n - 1]),
1002            ];
1003            let a_feasible = vec![0.0; n];
1004
1005            // Case 0: Time only
1006            let mut copp2_problem = Copp2ProblemBuilder::new(
1007                &robot,
1008                (0, n - 1),
1009                (0.0, 0.0),
1010                &[CoppObjective::Time(1.0)],
1011            )
1012            .build()?;
1013            let start = Instant::now();
1014            let mut a_case0 = copp2_socp(&copp2_problem, &options_socp)?;
1015            let tc_copp2_case0 = start.elapsed().as_secs_f64() * 1E3;
1016            robot
1017                .constraints
1018                .project_to_feasible_topp2(&mut a_case0, &a_feasible, 0)?;
1019            let (_, obj_case0) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case0);
1020
1021            // Case 1: Time and ThermalEnergy
1022            let obj_case1 = [
1023                CoppObjective::Time(1.0),
1024                CoppObjective::ThermalEnergy(1.0, &vec![1.0; dim]),
1025            ];
1026            copp2_problem.objectives = &obj_case1;
1027            let start = Instant::now();
1028            let mut a_case1 = copp2_socp(&copp2_problem, &options_socp)?;
1029            let tc_copp2_case1 = start.elapsed().as_secs_f64() * 1E3;
1030            robot
1031                .constraints
1032                .project_to_feasible_topp2(&mut a_case1, &a_feasible, 0)?;
1033            let (_, obj_case1) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case1);
1034            if obj_case1[0] < obj_case0[0] - 1E-3 || obj_case1[1] - 1E-3 > obj_case0[1] {
1035                let (tf_case0, _) = s_to_t_topp2(s.as_slice(), &a_case0, 0.0);
1036                let (tf_case1, _) = s_to_t_topp2(s.as_slice(), &a_case1, 0.0);
1037                crate::verbosity_log!(
1038                    crate::diag::Verbosity::Summary,
1039                    "omega = {omega:?}\nphi = {phi:?}"
1040                );
1041                crate::verbosity_log!(
1042                    crate::diag::Verbosity::Summary,
1043                    "Case 0: obj_time = {:.6}, obj_thermal_energy = {:.6}, tf = {:.6}",
1044                    obj_case0[0],
1045                    obj_case0[1],
1046                    tf_case0
1047                );
1048                crate::verbosity_log!(
1049                    crate::diag::Verbosity::Summary,
1050                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, tf = {:.6}",
1051                    obj_case1[0],
1052                    obj_case1[1],
1053                    tf_case1
1054                );
1055                crate::verbosity_log!(
1056                    crate::diag::Verbosity::Summary,
1057                    "Interesting... Cases 0 and 1"
1058                );
1059            }
1060
1061            // Case 2: Time and More ThermalEnergy
1062            let obj_case2 = [
1063                CoppObjective::Time(1.0),
1064                CoppObjective::ThermalEnergy(10.0, &vec![1.0; dim]),
1065            ];
1066            copp2_problem.objectives = &obj_case2;
1067            let start = Instant::now();
1068            let mut a_case2 = copp2_socp(&copp2_problem, &options_socp)?;
1069            let tc_copp2_case2 = start.elapsed().as_secs_f64() * 1E3;
1070            robot
1071                .constraints
1072                .project_to_feasible_topp2(&mut a_case2, &a_feasible, 0)?;
1073            let (_, obj_case2) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case2);
1074            if obj_case2[0] < obj_case1[0] - 1E-3 || obj_case2[1] - 1E-3 > obj_case1[1] {
1075                crate::verbosity_log!(
1076                    crate::diag::Verbosity::Summary,
1077                    "omega = {omega:?}\nphi = {phi:?}"
1078                );
1079                crate::verbosity_log!(
1080                    crate::diag::Verbosity::Summary,
1081                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}",
1082                    obj_case1[0],
1083                    obj_case1[1]
1084                );
1085                crate::verbosity_log!(
1086                    crate::diag::Verbosity::Summary,
1087                    "Case 2: obj_time = {:.6}, obj_thermal_energy = {:.6}",
1088                    obj_case2[0],
1089                    obj_case2[1]
1090                );
1091                crate::verbosity_log!(
1092                    crate::diag::Verbosity::Summary,
1093                    "Interesting... Cases 1 and 2"
1094                );
1095            }
1096
1097            // Case 3: Time and TotalVariationTorque
1098            let obj_case3 = [
1099                CoppObjective::Time(1.0),
1100                CoppObjective::TotalVariationTorque(1.0, &vec![1.0; dim]),
1101            ];
1102            copp2_problem.objectives = &obj_case3;
1103            let start = Instant::now();
1104            let mut a_case3 = copp2_socp(&copp2_problem, &options_socp)?;
1105            let tc_copp2_case3 = start.elapsed().as_secs_f64() * 1E3;
1106            robot
1107                .constraints
1108                .project_to_feasible_topp2(&mut a_case3, &a_feasible, 0)?;
1109            let (_, obj_case3) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case3);
1110            if obj_case3[1] < obj_case1[1] - 1E-3 || obj_case3[2] - 1E-3 > obj_case1[2] {
1111                crate::verbosity_log!(
1112                    crate::diag::Verbosity::Summary,
1113                    "omega = {omega:?}\nphi = {phi:?}"
1114                );
1115                crate::verbosity_log!(
1116                    crate::diag::Verbosity::Summary,
1117                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_total_variation_torque = {:.6}",
1118                    obj_case1[0],
1119                    obj_case1[1],
1120                    obj_case1[2]
1121                );
1122                crate::verbosity_log!(
1123                    crate::diag::Verbosity::Summary,
1124                    "Case 3: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_total_variation_torque = {:.6}",
1125                    obj_case3[0],
1126                    obj_case3[1],
1127                    obj_case3[2]
1128                );
1129                crate::verbosity_log!(
1130                    crate::diag::Verbosity::Summary,
1131                    "Interesting... Cases 1 and 3"
1132                );
1133            }
1134
1135            // Case 4: Time and Linear
1136            let obj_case4 = [
1137                CoppObjective::Time(1.0),
1138                CoppObjective::Linear(1.0, &vec![0.0; n], &vec![1.0; n - 1]),
1139            ];
1140            copp2_problem.objectives = &obj_case4;
1141            let start = Instant::now();
1142            let mut a_case4 = copp2_socp(&copp2_problem, &options_socp)?;
1143            let tc_copp2_case4 = start.elapsed().as_secs_f64() * 1E3;
1144            robot
1145                .constraints
1146                .project_to_feasible_topp2(&mut a_case4, &a_feasible, 0)?;
1147            let (_, obj_case4) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case4);
1148            if obj_case4[1] < obj_case1[1] - 1E-3 || obj_case4[3] - 1E-3 > obj_case1[3] {
1149                crate::verbosity_log!(
1150                    crate::diag::Verbosity::Summary,
1151                    "omega = {omega:?}\nphi = {phi:?}"
1152                );
1153                crate::verbosity_log!(
1154                    crate::diag::Verbosity::Summary,
1155                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_linear = {:.6}",
1156                    obj_case1[0],
1157                    obj_case1[1],
1158                    obj_case1[3]
1159                );
1160                crate::verbosity_log!(
1161                    crate::diag::Verbosity::Summary,
1162                    "Case 4: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_linear = {:.6}",
1163                    obj_case4[0],
1164                    obj_case4[1],
1165                    obj_case4[3]
1166                );
1167                crate::verbosity_log!(
1168                    crate::diag::Verbosity::Summary,
1169                    "Interesting... Cases 1 and 4"
1170                );
1171            }
1172            if obj_case4[2] < obj_case2[2] - 1E-3 || obj_case4[3] - 1E-3 > obj_case2[3] {
1173                crate::verbosity_log!(
1174                    crate::diag::Verbosity::Summary,
1175                    "omega = {omega:?}\nphi = {phi:?}"
1176                );
1177                crate::verbosity_log!(
1178                    crate::diag::Verbosity::Summary,
1179                    "Case 2: obj_time = {:.6}, obj_total_variation_torque = {:.6}, obj_linear = {:.6}",
1180                    obj_case2[0],
1181                    obj_case2[2],
1182                    obj_case2[3]
1183                );
1184                crate::verbosity_log!(
1185                    crate::diag::Verbosity::Summary,
1186                    "Case 4: obj_time = {:.6}, obj_total_variation_torque = {:.6}, obj_linear = {:.6}",
1187                    obj_case4[0],
1188                    obj_case4[2],
1189                    obj_case4[3]
1190                );
1191                crate::verbosity_log!(
1192                    crate::diag::Verbosity::Summary,
1193                    "Interesting... Cases 2 and 4"
1194                );
1195            }
1196
1197            if flag_print_step {
1198                crate::verbosity_log!(
1199                    crate::diag::Verbosity::Summary,
1200                    "Exp #{}:\n Case 0: tc={:.3}ms, obj={:?}\n Case 1: tc={:.3}ms, obj={:?}\n Case 2: tc={:.3}ms, obj={:?}\n Case 3: tc={:.3}ms, obj={:?}\n Case 4: tc={:.3}ms, obj={:?}",
1201                    i_exp + 1,
1202                    tc_copp2_case0,
1203                    obj_case0,
1204                    tc_copp2_case1,
1205                    obj_case1,
1206                    tc_copp2_case2,
1207                    obj_case2,
1208                    tc_copp2_case3,
1209                    obj_case3,
1210                    tc_copp2_case4,
1211                    obj_case4
1212                );
1213            }
1214
1215            tc_sum_case0 += tc_copp2_case0;
1216            tc_sum_case1 += tc_copp2_case1;
1217            tc_sum_case2 += tc_copp2_case2;
1218            tc_sum_case3 += tc_copp2_case3;
1219            tc_sum_case4 += tc_copp2_case4;
1220            for i in 0..obj_case0.len() {
1221                obj_sum_case0[i] += obj_case0[i];
1222                obj_sum_case1[i] += obj_case1[i];
1223                obj_sum_case2[i] += obj_case2[i];
1224                obj_sum_case3[i] += obj_case3[i];
1225                obj_sum_case4[i] += obj_case4[i];
1226            }
1227        }
1228
1229        for i in 0..4 {
1230            obj_sum_case0[i] /= n_exp as f64;
1231            obj_sum_case1[i] /= n_exp as f64;
1232            obj_sum_case2[i] /= n_exp as f64;
1233            obj_sum_case3[i] /= n_exp as f64;
1234            obj_sum_case4[i] /= n_exp as f64;
1235        }
1236
1237        crate::verbosity_log!(
1238            crate::diag::Verbosity::Summary,
1239            "Average {} experiments:\n Case 0: tc={:.3}ms, obj={:?}\n Case 1: tc={:.3}ms, obj={:?}\n Case 2: tc={:.3}ms, obj={:?}\n Case 3: tc={:.3}ms, obj={:?}\n Case 4: tc={:.3}ms, obj={:?}",
1240            n_exp,
1241            tc_sum_case0 / n_exp as f64,
1242            obj_sum_case0,
1243            tc_sum_case1 / n_exp as f64,
1244            obj_sum_case1,
1245            tc_sum_case2 / n_exp as f64,
1246            obj_sum_case2,
1247            tc_sum_case3 / n_exp as f64,
1248            obj_sum_case3,
1249            tc_sum_case4 / n_exp as f64,
1250            obj_sum_case4
1251        );
1252
1253        Ok(())
1254    }
1255
1256    fn run_one_copp2_socp_only_time_case(
1257        options_ra: &ReachSet2Options,
1258        options_socp: &ClarabelOptions,
1259    ) -> Result<(f64, f64, f64, f64, f64, f64), CoppError> {
1260        let n: usize = 1000;
1261        let mut robot = Robot::with_capacity(Plannar2LinkEnd::new(1.0, 1.0, 1.0, 1.0), n);
1262        let dim = robot.dim();
1263
1264        let mut rng = rand::rng();
1265        let (s, derivs, omega, phi) =
1266            lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
1267        robot.with_s(&s.as_view())?;
1268        robot.with_q(
1269            &derivs.q.as_view(),
1270            &derivs.dq.as_ref().unwrap().as_view(),
1271            &derivs.ddq.as_ref().unwrap().as_view(),
1272            derivs.dddq.as_ref().map(|m| m.as_view()).as_ref(),
1273            0,
1274        )?;
1275        add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
1276
1277        let topp2_problem =
1278            Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
1279        let start = Instant::now();
1280        let a_ra = topp2_ra(&topp2_problem, options_ra)?;
1281        let tc_ra = start.elapsed().as_secs_f64() * 1E3;
1282        let (tf_ra, _) = s_to_t_topp2(s.as_slice(), &a_ra, 0.0);
1283
1284        let obj1 = [CoppObjective::Linear(
1285            1.0,
1286            &vec![-1.0; n],
1287            &vec![0.0; n - 1],
1288        )];
1289        let mut copp2_problem =
1290            Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &obj1).build()?;
1291        let start = Instant::now();
1292        let a_lp = copp2_socp(&copp2_problem, options_socp)?;
1293        let tc_lp = start.elapsed().as_secs_f64() * 1E3;
1294        let (tf_lp, _) = s_to_t_topp2(s.as_slice(), &a_lp, 0.0);
1295
1296        copp2_problem.objectives = &[CoppObjective::Time(1.0)];
1297        let start = Instant::now();
1298        let a_qp = copp2_socp(&copp2_problem, options_socp)?;
1299        let tc_qp = start.elapsed().as_secs_f64() * 1E3;
1300        let (tf_qp, _) = s_to_t_topp2(s.as_slice(), &a_qp, 0.0);
1301
1302        if (tf_lp - tf_ra).abs() > 1e-3 || (tf_qp - tf_ra).abs() > 1e-3 {
1303            crate::verbosity_log!(
1304                crate::diag::Verbosity::Summary,
1305                "omega = {omega:?}\nphi = {phi:?}"
1306            );
1307            panic!("COPP2 time optimality failed!");
1308        }
1309
1310        Ok((tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp))
1311    }
1312
1313    fn run_test_copp2_socp_only_time_repeated(
1314        n_exp: usize,
1315        flag_print_step: bool,
1316    ) -> Result<(), CoppError> {
1317        let mut tc_sum_ra = 0.0;
1318        let mut tc_sum_lp = 0.0;
1319        let mut tc_sum_qp = 0.0;
1320        let mut tf_sum_ra = 0.0;
1321        let mut tf_sum_lp = 0.0;
1322        let mut tf_sum_qp = 0.0;
1323
1324        let options_ra = ReachSet2OptionsBuilder::new()
1325            .lp_feas_tol(1E-9)
1326            .a_cmp_abs_tol(1E-9)
1327            .a_cmp_rel_tol(1E-9)
1328            .build()?;
1329        let options_socp = ClarabelOptionsBuilder::new()
1330            .allow_almost_solved(true)
1331            .build()?;
1332
1333        for i_exp in 0..n_exp {
1334            let (tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp) =
1335                run_one_copp2_socp_only_time_case(&options_ra, &options_socp)?;
1336
1337            if flag_print_step {
1338                crate::verbosity_log!(
1339                    crate::diag::Verbosity::Summary,
1340                    "Exp #{}: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, tf_qp = {:.6}",
1341                    i_exp + 1,
1342                    tc_ra,
1343                    tc_lp,
1344                    tc_qp,
1345                    tf_ra,
1346                    tf_lp,
1347                    tf_qp,
1348                );
1349            }
1350
1351            tc_sum_ra += tc_ra;
1352            tc_sum_lp += tc_lp;
1353            tc_sum_qp += tc_qp;
1354            tf_sum_ra += tf_ra;
1355            tf_sum_lp += tf_lp;
1356            tf_sum_qp += tf_qp;
1357        }
1358
1359        crate::verbosity_log!(
1360            crate::diag::Verbosity::Summary,
1361            "Average {} experiments: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, tf_qp = {:.6}",
1362            n_exp,
1363            tc_sum_ra / n_exp as f64,
1364            tc_sum_lp / n_exp as f64,
1365            tc_sum_qp / n_exp as f64,
1366            tf_sum_ra / n_exp as f64,
1367            tf_sum_lp / n_exp as f64,
1368            tf_sum_qp / n_exp as f64
1369        );
1370
1371        Ok(())
1372    }
1373}